"""Batch Device Configuration Manager.

Provides comprehensive batch configuration management for multiple devices,
including device discovery, configuration distribution, progress monitoring,
and error handling with retry mechanisms.
"""

import asyncio
import json
import logging
import time
import uuid
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import Dict, List, Optional, AsyncIterator, Callable, Any, Set
from pathlib import Path

logger = logging.getLogger(__name__)


# Configuration Constants
BATCH_CONFIG = {
    "max_concurrent_devices": 50,
    "batch_size": 10,
    "timeout_per_device": 120,
    "max_retry_attempts": 3,
    "retry_delay": 5,
    "progress_update_interval": 2,
    "log_level": "INFO",
    "enable_rollback": True,
    "connection_pool_size": 20,
    "memory_usage_limit": 200,
    "cpu_usage_limit": 80
}


class JobStatus(Enum):
    """Batch configuration job status."""
    PENDING = "pending"
    SCANNING = "scanning"
    CONFIGURING = "configuring"
    PAUSED = "paused"
    COMPLETED = "completed"
    FAILED = "failed"
    CANCELLED = "cancelled"


class DeviceConfigStatus(Enum):
    """Individual device configuration status."""
    PENDING = "pending"
    CONNECTING = "connecting"
    AUTHENTICATING = "authenticating"
    CONFIGURING = "configuring"
    VALIDATING = "validating"
    SUCCESS = "success"
    FAILED = "failed"
    RETRYING = "retrying"
    SKIPPED = "skipped"


class RetryDecision(Enum):
    """Retry decision for failed configurations."""
    RETRY_IMMEDIATELY = "retry_now"
    RETRY_WITH_DELAY = "retry_delay"
    SKIP_DEVICE = "skip"
    ABORT_JOB = "abort"
    ISOLATE_DEVICE = "isolate"


@dataclass
class DeviceInfo:
    """Device information."""
    device_id: str
    name: str
    mac_address: str
    signal_strength: int
    device_type: str
    firmware_version: str
    last_seen: datetime
    location_tag: Optional[str] = None
    is_configured: bool = False
    is_online: bool = True


@dataclass
class DeviceFilter:
    """Device selection filter."""
    device_types: Optional[List[str]] = None
    firmware_versions: Optional[List[str]] = None
    signal_strength: Optional[int] = None
    location_tags: Optional[List[str]] = None
    last_seen_hours: Optional[int] = None
    exclude_configured: bool = True
    exclude_offline: bool = True


@dataclass
class DeviceConfigResult:
    """Device configuration result."""
    device_id: str
    device_name: str
    mac_address: str
    status: DeviceConfigStatus
    start_time: datetime
    end_time: Optional[datetime] = None
    duration: float = 0.0
    retry_count: int = 0
    error_code: Optional[str] = None
    error_message: Optional[str] = None
    config_checksum: Optional[str] = None
    rollback_info: Optional[Dict[str, Any]] = None


@dataclass
class BatchConfigJob:
    """Batch configuration job."""
    job_id: str
    name: str
    description: str = ""
    target_filter: Optional[DeviceFilter] = None
    config_data: Dict[str, Any] = field(default_factory=dict)
    created_by: str = "system"
    created_at: datetime = field(default_factory=datetime.now)
    status: JobStatus = JobStatus.PENDING
    
    # Execution status
    started_at: Optional[datetime] = None
    completed_at: Optional[datetime] = None
    total_devices: int = 0
    processed_devices: int = 0
    success_devices: int = 0
    failed_devices: int = 0
    
    # Results
    device_results: List[DeviceConfigResult] = field(default_factory=list)
    error_summary: Dict[str, int] = field(default_factory=dict)
    performance_metrics: Dict[str, float] = field(default_factory=dict)


@dataclass
class ProgressInfo:
    """Progress information."""
    job_id: str
    status: JobStatus
    progress_percentage: float
    current_phase: str
    devices_total: int
    devices_completed: int
    devices_failed: int
    estimated_completion: Optional[datetime] = None
    current_throughput: float = 0.0  # devices per minute
    average_device_time: float = 0.0  # average time per device
    error_rate: float = 0.0


@dataclass
class ErrorClassification:
    """Error classification result."""
    error_type: str
    category: str
    severity: str
    is_transient: bool
    is_recoverable: bool
    estimated_fix_time: int
    suggested_action: str


class DeviceDiscovery:
    """Device discovery and selection manager."""
    
    def __init__(self, ble_scanner, device_registry):
        """Initialize device discovery.
        
        Args:
            ble_scanner: BLE scanner instance
            device_registry: Device registry instance
        """
        self.ble_scanner = ble_scanner
        self.device_registry = device_registry
        self.discovered_devices: Dict[str, DeviceInfo] = {}
        self.scan_filters: List[DeviceFilter] = []
        self.scan_active = False
        
    async def start_batch_scan(self, duration: int = 30,
                              filters: Optional[List[DeviceFilter]] = None) -> List[DeviceInfo]:
        """Start batch device scanning.
        
        Args:
            duration: Scan duration in seconds
            filters: Device filters to apply
            
        Returns:
            List of discovered devices
        """
        try:
            logger.info(f"Starting batch scan for {duration} seconds")
            
            self.scan_filters = filters or []
            self.discovered_devices.clear()
            self.scan_active = True
            
            # Start BLE scanning
            scan_task = asyncio.create_task(
                self._perform_scan(duration)
            )
            
            # Wait for scan completion
            await scan_task
            
            # Apply filters
            filtered_devices = self._apply_filters(
                list(self.discovered_devices.values())
            )
            
            logger.info(f"Discovered {len(filtered_devices)} devices")
            return filtered_devices
            
        except Exception as e:
            logger.error(f"Batch scan error: {e}")
            return []
        finally:
            self.scan_active = False
    
    async def _perform_scan(self, duration: int):
        """Perform BLE scan for specified duration."""
        end_time = time.time() + duration
        
        while time.time() < end_time and self.scan_active:
            try:
                # Scan for devices
                devices = await self.ble_scanner.scan(timeout=2)
                
                for device_data in devices:
                    device_info = self._parse_device_data(device_data)
                    if device_info:
                        self.discovered_devices[device_info.device_id] = device_info
                        
                await asyncio.sleep(1)
                
            except Exception as e:
                logger.warning(f"Scan iteration error: {e}")
                await asyncio.sleep(2)
    
    def _parse_device_data(self, device_data: Dict[str, Any]) -> Optional[DeviceInfo]:
        """Parse raw device data into DeviceInfo."""
        try:
            return DeviceInfo(
                device_id=device_data.get('device_id', ''),
                name=device_data.get('name', 'Unknown Device'),
                mac_address=device_data.get('address', ''),
                signal_strength=device_data.get('rssi', -100),
                device_type=device_data.get('device_type', 'unknown'),
                firmware_version=device_data.get('firmware_version', '0.0.0'),
                last_seen=datetime.now(),
                location_tag=device_data.get('location_tag'),
                is_configured=device_data.get('is_configured', False),
                is_online=True
            )
        except Exception as e:
            logger.warning(f"Failed to parse device data: {e}")
            return None
    
    def _apply_filters(self, devices: List[DeviceInfo]) -> List[DeviceInfo]:
        """Apply device filters."""
        filtered_devices = devices
        
        for device_filter in self.scan_filters:
            filtered_devices = self._apply_single_filter(filtered_devices, device_filter)
        
        return filtered_devices
    
    def _apply_single_filter(self, devices: List[DeviceInfo], 
                           device_filter: DeviceFilter) -> List[DeviceInfo]:
        """Apply single device filter."""
        filtered = []
        
        for device in devices:
            if self._device_matches_filter(device, device_filter):
                filtered.append(device)
                
        return filtered
    
    def _device_matches_filter(self, device: DeviceInfo, 
                             device_filter: DeviceFilter) -> bool:
        """Check if device matches filter criteria."""
        # Device type filter
        if (device_filter.device_types and 
            device.device_type not in device_filter.device_types):
            return False
        
        # Firmware version filter
        if (device_filter.firmware_versions and 
            device.firmware_version not in device_filter.firmware_versions):
            return False
        
        # Signal strength filter
        if (device_filter.signal_strength and 
            device.signal_strength < device_filter.signal_strength):
            return False
        
        # Location tag filter
        if (device_filter.location_tags and 
            device.location_tag not in device_filter.location_tags):
            return False
        
        # Last seen filter
        if device_filter.last_seen_hours:
            hours_ago = datetime.now() - timedelta(hours=device_filter.last_seen_hours)
            if device.last_seen < hours_ago:
                return False
        
        # Configured status filter
        if device_filter.exclude_configured and device.is_configured:
            return False
        
        # Online status filter
        if device_filter.exclude_offline and not device.is_online:
            return False
        
        return True
    
    def group_devices(self, devices: List[DeviceInfo], 
                     group_by: str = "location") -> Dict[str, List[DeviceInfo]]:
        """Group devices by specified criteria."""
        groups: Dict[str, List[DeviceInfo]] = {}
        
        for device in devices:
            if group_by == "location":
                key = device.location_tag or "unknown"
            elif group_by == "type":
                key = device.device_type
            elif group_by == "firmware":
                key = device.firmware_version
            else:
                key = "default"
            
            if key not in groups:
                groups[key] = []
            groups[key].append(device)
        
        return groups


class ErrorHandler:
    """Error handling and retry logic."""
    
    def __init__(self):
        """Initialize error handler."""
        self.error_patterns = self._load_error_patterns()
        self.retry_strategies = self._load_retry_strategies()
        
    def _load_error_patterns(self) -> Dict[str, Dict[str, Any]]:
        """Load error patterns for classification."""
        return {
            "connection_timeout": {
                "category": "network",
                "severity": "medium",
                "is_transient": True,
                "is_recoverable": True,
                "estimated_fix_time": 30,
                "suggested_action": "retry_with_delay"
            },
            "authentication_failed": {
                "category": "security", 
                "severity": "high",
                "is_transient": False,
                "is_recoverable": True,
                "estimated_fix_time": 300,
                "suggested_action": "skip_device"
            },
            "device_offline": {
                "category": "connectivity",
                "severity": "low", 
                "is_transient": True,
                "is_recoverable": True,
                "estimated_fix_time": 60,
                "suggested_action": "retry_with_delay"
            },
            "config_invalid": {
                "category": "validation",
                "severity": "high",
                "is_transient": False,
                "is_recoverable": False,
                "estimated_fix_time": 600,
                "suggested_action": "skip_device"
            },
            "memory_error": {
                "category": "resource",
                "severity": "high",
                "is_transient": True,
                "is_recoverable": True,
                "estimated_fix_time": 10,
                "suggested_action": "retry_immediately"
            }
        }
    
    def _load_retry_strategies(self) -> Dict[str, Dict[str, Any]]:
        """Load retry strategies for different error types."""
        return {
            "network": {
                "max_retries": 3,
                "base_delay": 5,
                "backoff_factor": 2,
                "max_delay": 60
            },
            "security": {
                "max_retries": 1,
                "base_delay": 10,
                "backoff_factor": 1,
                "max_delay": 10
            },
            "connectivity": {
                "max_retries": 2,
                "base_delay": 10,
                "backoff_factor": 1.5,
                "max_delay": 30
            },
            "validation": {
                "max_retries": 0,
                "base_delay": 0,
                "backoff_factor": 1,
                "max_delay": 0
            },
            "resource": {
                "max_retries": 2,
                "base_delay": 2,
                "backoff_factor": 1,
                "max_delay": 5
            }
        }
    
    def classify_error(self, error: Exception, 
                      context: Dict[str, Any]) -> ErrorClassification:
        """Classify error and determine handling strategy."""
        error_message = str(error).lower()
        error_type = "unknown"
        
        # Match error patterns
        for pattern, config in self.error_patterns.items():
            if pattern in error_message or pattern in error.__class__.__name__.lower():
                error_type = pattern
                break
        
        # Get error configuration
        error_config = self.error_patterns.get(error_type, {
            "category": "unknown",
            "severity": "medium",
            "is_transient": True,
            "is_recoverable": True,
            "estimated_fix_time": 30,
            "suggested_action": "retry_with_delay"
        })
        
        return ErrorClassification(
            error_type=error_type,
            category=error_config["category"],
            severity=error_config["severity"],
            is_transient=error_config["is_transient"],
            is_recoverable=error_config["is_recoverable"],
            estimated_fix_time=error_config["estimated_fix_time"],
            suggested_action=error_config["suggested_action"]
        )
    
    def calculate_retry_delay(self, attempt: int, error_type: str) -> float:
        """Calculate retry delay based on attempt number and error type."""
        error_config = self.error_patterns.get(error_type, {})
        category = error_config.get("category", "unknown")
        
        strategy = self.retry_strategies.get(category, {
            "base_delay": 5,
            "backoff_factor": 2,
            "max_delay": 60
        })
        
        delay = strategy["base_delay"] * (strategy["backoff_factor"] ** (attempt - 1))
        return min(delay, strategy["max_delay"])
    
    def should_retry(self, device_id: str, attempt: int, 
                    error_classification: ErrorClassification) -> RetryDecision:
        """Determine retry decision based on error classification."""
        category = error_classification.category
        strategy = self.retry_strategies.get(category, {"max_retries": 1})
        
        if attempt >= strategy["max_retries"]:
            return RetryDecision.SKIP_DEVICE
        
        if not error_classification.is_recoverable:
            return RetryDecision.SKIP_DEVICE
        
        suggested_action = error_classification.suggested_action
        
        if suggested_action == "retry_immediately":
            return RetryDecision.RETRY_IMMEDIATELY
        elif suggested_action == "retry_with_delay":
            return RetryDecision.RETRY_WITH_DELAY
        elif suggested_action == "skip_device":
            return RetryDecision.SKIP_DEVICE
        elif suggested_action == "abort_job":
            return RetryDecision.ABORT_JOB
        else:
            return RetryDecision.RETRY_WITH_DELAY


class ConfigDistributor:
    """Configuration distribution manager."""
    
    def __init__(self, connection_manager, security_manager, validator):
        """Initialize config distributor.
        
        Args:
            connection_manager: BLE connection manager
            security_manager: Security manager
            validator: Configuration validator
        """
        self.connection_manager = connection_manager
        self.security_manager = security_manager
        self.validator = validator
        self.active_sessions: Dict[str, Any] = {}
        self.config_queue = asyncio.Queue()
        self.semaphore = asyncio.Semaphore(BATCH_CONFIG["max_concurrent_devices"])
        
    async def configure_device(self, device_info: DeviceInfo, 
                             config_data: Dict[str, Any]) -> DeviceConfigResult:
        """Configure a single device.
        
        Args:
            device_info: Device information
            config_data: Configuration data
            
        Returns:
            Configuration result
        """
        device_id = device_info.device_id
        result = DeviceConfigResult(
            device_id=device_id,
            device_name=device_info.name,
            mac_address=device_info.mac_address,
            status=DeviceConfigStatus.PENDING,
            start_time=datetime.now()
        )
        
        async with self.semaphore:
            try:
                # Update status to connecting
                result.status = DeviceConfigStatus.CONNECTING
                logger.info(f"Connecting to device: {device_id}")
                
                # Establish connection
                connection = await self.connection_manager.connect(
                    device_info.mac_address, timeout=30
                )
                
                if not connection:
                    raise Exception(f"Failed to connect to device {device_id}")
                
                # Update status to authenticating
                result.status = DeviceConfigStatus.AUTHENTICATING
                logger.debug(f"Authenticating device: {device_id}")
                
                # Authenticate if required
                session = await self.security_manager.authenticate_device(
                    device_id, connection
                )
                
                if not session:
                    raise Exception(f"Authentication failed for device {device_id}")
                
                # Update status to configuring
                result.status = DeviceConfigStatus.CONFIGURING
                logger.debug(f"Configuring device: {device_id}")
                
                # Apply configuration
                success = await self._apply_configuration(
                    connection, config_data
                )
                
                if not success:
                    raise Exception(f"Configuration application failed for device {device_id}")
                
                # Update status to validating
                result.status = DeviceConfigStatus.VALIDATING
                logger.debug(f"Validating configuration for device: {device_id}")
                
                # Validate configuration
                validation_result = await self._validate_configuration(
                    connection, config_data
                )
                
                if not validation_result.is_valid:
                    raise Exception(f"Configuration validation failed: {validation_result.error}")
                
                # Success
                result.status = DeviceConfigStatus.SUCCESS
                result.config_checksum = validation_result.checksum
                logger.info(f"Successfully configured device: {device_id}")
                
            except Exception as e:
                result.status = DeviceConfigStatus.FAILED
                result.error_message = str(e)
                logger.error(f"Device configuration failed {device_id}: {e}")
                
            finally:
                result.end_time = datetime.now()
                result.duration = (result.end_time - result.start_time).total_seconds()
                
                # Cleanup connection
                try:
                    if 'connection' in locals() and connection:
                        await self.connection_manager.disconnect(connection)
                except Exception as e:
                    logger.warning(f"Failed to cleanup connection for {device_id}: {e}")
        
        return result
    
    async def _apply_configuration(self, connection, config_data: Dict[str, Any]) -> bool:
        """Apply configuration to device."""
        try:
            # WiFi configuration
            if "wifi" in config_data:
                wifi_config = config_data["wifi"]
                success = await connection.configure_wifi(
                    ssid=wifi_config.get("ssid"),
                    password=wifi_config.get("password"),
                    security=wifi_config.get("security", "WPA2")
                )
                if not success:
                    return False
            
            # System configuration
            if "system" in config_data:
                system_config = config_data["system"]
                success = await connection.configure_system(system_config)
                if not success:
                    return False
            
            # Application configuration
            if "application" in config_data:
                app_config = config_data["application"]
                success = await connection.configure_application(app_config)
                if not success:
                    return False
            
            return True
            
        except Exception as e:
            logger.error(f"Configuration application error: {e}")
            return False
    
    async def _validate_configuration(self, connection, 
                                    expected_config: Dict[str, Any]):
        """Validate applied configuration."""
        try:
            # Get current configuration
            current_config = await connection.get_current_configuration()
            
            # Validate using validator
            validation_result = await self.validator.validate_configuration(
                current_config, expected_config
            )
            
            return validation_result
            
        except Exception as e:
            logger.error(f"Configuration validation error: {e}")
            return self.validator.create_invalid_result(str(e))


class JobController:
    """Batch job controller and progress manager."""
    
    def __init__(self, state_manager, notification_service):
        """Initialize job controller.
        
        Args:
            state_manager: Job state manager
            notification_service: Progress notification service
        """
        self.state_manager = state_manager
        self.notification_service = notification_service
        self.active_jobs: Dict[str, BatchConfigJob] = {}
        self.job_tasks: Dict[str, asyncio.Task] = {}
        
    async def start_job(self, job: BatchConfigJob) -> str:
        """Start batch configuration job.
        
        Args:
            job: Batch configuration job
            
        Returns:
            Job ID
        """
        job_id = job.job_id
        
        try:
            # Update job status
            job.status = JobStatus.SCANNING
            job.started_at = datetime.now()
            
            # Store job
            self.active_jobs[job_id] = job
            await self.state_manager.save_job_state(job)
            
            # Start job task
            job_task = asyncio.create_task(self._execute_job(job))
            self.job_tasks[job_id] = job_task
            
            logger.info(f"Started batch configuration job: {job_id}")
            return job_id
            
        except Exception as e:
            logger.error(f"Failed to start job {job_id}: {e}")
            job.status = JobStatus.FAILED
            await self.state_manager.save_job_state(job)
            raise
    
    async def pause_job(self, job_id: str) -> bool:
        """Pause running job.
        
        Args:
            job_id: Job identifier
            
        Returns:
            True if paused successfully
        """
        try:
            job = self.active_jobs.get(job_id)
            if not job:
                return False
            
            if job.status != JobStatus.CONFIGURING:
                return False
            
            job.status = JobStatus.PAUSED
            await self.state_manager.save_job_state(job)
            
            logger.info(f"Paused job: {job_id}")
            return True
            
        except Exception as e:
            logger.error(f"Failed to pause job {job_id}: {e}")
            return False
    
    async def resume_job(self, job_id: str) -> bool:
        """Resume paused job.
        
        Args:
            job_id: Job identifier
            
        Returns:
            True if resumed successfully
        """
        try:
            job = self.active_jobs.get(job_id)
            if not job:
                return False
            
            if job.status != JobStatus.PAUSED:
                return False
            
            job.status = JobStatus.CONFIGURING
            await self.state_manager.save_job_state(job)
            
            logger.info(f"Resumed job: {job_id}")
            return True
            
        except Exception as e:
            logger.error(f"Failed to resume job {job_id}: {e}")
            return False
    
    async def cancel_job(self, job_id: str, reason: str = None) -> bool:
        """Cancel running job.
        
        Args:
            job_id: Job identifier
            reason: Cancellation reason
            
        Returns:
            True if cancelled successfully
        """
        try:
            job = self.active_jobs.get(job_id)
            if not job:
                return False
            
            # Cancel job task
            job_task = self.job_tasks.get(job_id)
            if job_task:
                job_task.cancel()
            
            # Update job status
            job.status = JobStatus.CANCELLED
            job.completed_at = datetime.now()
            
            if reason:
                job.error_summary["cancellation_reason"] = reason
            
            await self.state_manager.save_job_state(job)
            
            # Cleanup
            self.active_jobs.pop(job_id, None)
            self.job_tasks.pop(job_id, None)
            
            logger.info(f"Cancelled job: {job_id} (reason: {reason})")
            return True
            
        except Exception as e:
            logger.error(f"Failed to cancel job {job_id}: {e}")
            return False
    
    async def get_job_status(self, job_id: str) -> Optional[ProgressInfo]:
        """Get current job status and progress.
        
        Args:
            job_id: Job identifier
            
        Returns:
            Progress information or None if job not found
        """
        try:
            job = self.active_jobs.get(job_id)
            if not job:
                # Try to load from persistent storage
                job = await self.state_manager.load_job_state(job_id)
                if not job:
                    return None
            
            # Calculate progress metrics
            progress_percentage = 0.0
            if job.total_devices > 0:
                progress_percentage = (job.processed_devices / job.total_devices) * 100
            
            # Calculate throughput
            current_throughput = 0.0
            if job.started_at and job.processed_devices > 0:
                elapsed = (datetime.now() - job.started_at).total_seconds() / 60
                current_throughput = job.processed_devices / elapsed if elapsed > 0 else 0.0
            
            # Calculate average device time
            average_device_time = 0.0
            if job.device_results:
                total_time = sum(r.duration for r in job.device_results if r.duration > 0)
                completed_count = len([r for r in job.device_results if r.status == DeviceConfigStatus.SUCCESS])
                if completed_count > 0:
                    average_device_time = total_time / completed_count
            
            # Calculate error rate
            error_rate = 0.0
            if job.processed_devices > 0:
                error_rate = (job.failed_devices / job.processed_devices) * 100
            
            # Estimate completion time
            estimated_completion = None
            if (job.status == JobStatus.CONFIGURING and 
                current_throughput > 0 and 
                job.total_devices > job.processed_devices):
                remaining_devices = job.total_devices - job.processed_devices
                remaining_minutes = remaining_devices / current_throughput
                estimated_completion = datetime.now() + timedelta(minutes=remaining_minutes)
            
            return ProgressInfo(
                job_id=job_id,
                status=job.status,
                progress_percentage=progress_percentage,
                current_phase=self._get_current_phase(job),
                devices_total=job.total_devices,
                devices_completed=job.success_devices,
                devices_failed=job.failed_devices,
                estimated_completion=estimated_completion,
                current_throughput=current_throughput,
                average_device_time=average_device_time,
                error_rate=error_rate
            )
            
        except Exception as e:
            logger.error(f"Failed to get job status {job_id}: {e}")
            return None
    
    def _get_current_phase(self, job: BatchConfigJob) -> str:
        """Get current job phase description."""
        if job.status == JobStatus.SCANNING:
            return "Discovering devices"
        elif job.status == JobStatus.CONFIGURING:
            return f"Configuring devices ({job.processed_devices}/{job.total_devices})"
        elif job.status == JobStatus.PAUSED:
            return "Job paused"
        elif job.status == JobStatus.COMPLETED:
            return "Job completed"
        elif job.status == JobStatus.FAILED:
            return "Job failed"
        elif job.status == JobStatus.CANCELLED:
            return "Job cancelled"
        else:
            return "Preparing"
    
    async def _execute_job(self, job: BatchConfigJob):
        """Execute batch configuration job."""
        try:
            # This will be implemented in the actual execution logic
            # For now, simulate job execution
            await asyncio.sleep(1)
            
            job.status = JobStatus.COMPLETED
            job.completed_at = datetime.now()
            await self.state_manager.save_job_state(job)
            
        except asyncio.CancelledError:
            logger.info(f"Job {job.job_id} was cancelled")
            raise
        except Exception as e:
            logger.error(f"Job execution error {job.job_id}: {e}")
            job.status = JobStatus.FAILED
            await self.state_manager.save_job_state(job)
        finally:
            # Cleanup
            self.active_jobs.pop(job.job_id, None)
            self.job_tasks.pop(job.job_id, None)


class BatchConfigManager:
    """Main batch configuration manager.
    
    Coordinates device discovery, configuration distribution, progress monitoring,
    and error handling for batch configuration operations.
    """
    
    def __init__(self, ble_scanner, connection_manager, security_manager, 
                 validator, state_manager, notification_service, device_registry):
        """Initialize batch configuration manager.
        
        Args:
            ble_scanner: BLE scanner for device discovery
            connection_manager: BLE connection manager
            security_manager: Security and authentication manager
            validator: Configuration validator
            state_manager: Job state persistence manager
            notification_service: Progress notification service
            device_registry: Device registry for device information
        """
        self.device_discovery = DeviceDiscovery(ble_scanner, device_registry)
        self.config_distributor = ConfigDistributor(
            connection_manager, security_manager, validator
        )
        self.job_controller = JobController(state_manager, notification_service)
        self.error_handler = ErrorHandler()
        
        # Progress tracking
        self.progress_callbacks: List[Callable[[ProgressInfo], None]] = []
        self.executor = ThreadPoolExecutor(max_workers=4)
        
        logger.info("BatchConfigManager initialized")
    
    async def create_batch_job(self, name: str, config_data: Dict[str, Any],
                             device_filter: Optional[DeviceFilter] = None,
                             description: str = "") -> str:
        """Create new batch configuration job.
        
        Args:
            name: Job name
            config_data: Configuration data to apply
            device_filter: Device selection filter
            description: Job description
            
        Returns:
            Job ID
        """
        job_id = str(uuid.uuid4())
        
        job = BatchConfigJob(
            job_id=job_id,
            name=name,
            description=description,
            target_filter=device_filter,
            config_data=config_data,
            created_at=datetime.now()
        )
        
        logger.info(f"Created batch configuration job: {job_id}")
        return job_id
    
    async def execute_batch_job(self, job_id: str) -> bool:
        """Execute batch configuration job.
        
        Args:
            job_id: Job identifier
            
        Returns:
            True if job started successfully
        """
        try:
            # This would load the job from storage and execute it
            # Implementation will be completed in the actual execution logic
            logger.info(f"Executing batch job: {job_id}")
            return True
            
        except Exception as e:
            logger.error(f"Failed to execute batch job {job_id}: {e}")
            return False
    
    def add_progress_callback(self, callback: Callable[[ProgressInfo], None]):
        """Add progress update callback.
        
        Args:
            callback: Progress callback function
        """
        self.progress_callbacks.append(callback)
    
    def remove_progress_callback(self, callback: Callable[[ProgressInfo], None]):
        """Remove progress update callback.
        
        Args:
            callback: Progress callback function to remove
        """
        if callback in self.progress_callbacks:
            self.progress_callbacks.remove(callback)
    
    async def get_statistics(self) -> Dict[str, Any]:
        """Get batch configuration statistics.
        
        Returns:
            Statistics dictionary
        """
        return {
            "batch_manager": {
                "active_jobs": len(self.job_controller.active_jobs),
                "progress_callbacks": len(self.progress_callbacks),
                "max_concurrent_devices": BATCH_CONFIG["max_concurrent_devices"]
            }
        }
    
    async def cleanup(self):
        """Clean up batch configuration manager."""
        # Cancel all active jobs
        for job_id in list(self.job_controller.active_jobs.keys()):
            await self.job_controller.cancel_job(job_id, "System cleanup")
        
        # Shutdown executor
        self.executor.shutdown(wait=True)
        
        logger.info("BatchConfigManager cleanup completed")


# Example usage and testing
if __name__ == "__main__":
    import asyncio
    from unittest.mock import MagicMock
    
    async def test_batch_manager():
        """Test batch configuration manager."""
        # Create mock dependencies
        ble_scanner = MagicMock()
        connection_manager = MagicMock()
        security_manager = MagicMock()
        validator = MagicMock()
        state_manager = MagicMock()
        notification_service = MagicMock()
        device_registry = MagicMock()
        
        # Create batch manager
        batch_manager = BatchConfigManager(
            ble_scanner, connection_manager, security_manager,
            validator, state_manager, notification_service, device_registry
        )
        
        try:
            # Test job creation
            config_data = {
                "wifi": {
                    "ssid": "TestNetwork",
                    "password": "test123",
                    "security": "WPA2"
                },
                "system": {
                    "timezone": "UTC",
                    "ntp_server": "pool.ntp.org"
                }
            }
            
            device_filter = DeviceFilter(
                device_types=["printer"],
                exclude_configured=True,
                exclude_offline=True
            )
            
            job_id = await batch_manager.create_batch_job(
                name="Test Batch Configuration",
                config_data=config_data,
                device_filter=device_filter,
                description="Test batch configuration job"
            )
            
            print(f"Created job: {job_id}")
            
            # Test statistics
            stats = await batch_manager.get_statistics()
            print(f"Statistics: {json.dumps(stats, indent=2)}")
            
        finally:
            await batch_manager.cleanup()
    
    # Run test
    asyncio.run(test_batch_manager())